package com.guodefu.demo.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.guodefu.jackson.config.CustomJacksonModule;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
public class CacheConfig {

    private CacheConfigProperties cacheConfigProperties;

    public CacheConfig(CacheConfigProperties cacheConfigProperties) {
        this.cacheConfigProperties = cacheConfigProperties;
    }

    public static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    @Bean
    public StringRedisSerializer stringRedisSerializer() {
        return new StringRedisSerializer();
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory, RedisTemplate<String, ?> redisTemplate) {
        String defaultTtl = cacheConfigProperties.getDefaultTtl();
        // 设置默认缓存配置
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(name -> name + ":") //变双冒号为单冒号,和redisTemplate约定一致
                .entryTtl(Duration.parse(defaultTtl)) // 设置缓存过期时间
                .disableCachingNullValues() // 不缓存null值
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getStringSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));

        Map<String, String> cacheNames = cacheConfigProperties.getCacheNames();
        // 解析配置项，为某些缓存设置特殊的过期策略
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        // RedisCacheConfiguration 采用了“不可变对象模式”
        // 每次调用 entryTtl(Duration ttl) 这样的修改方法时，它不会改变当前对象，而是基于当前配置创建一个新的对象，应用更改后的配置。
        cacheNames.forEach((key, value) -> cacheConfigurations.put(key, defaultCacheConfig.entryTtl(Duration.parse(value))));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(defaultCacheConfig)
                .withInitialCacheConfigurations(cacheConfigurations)
                .transactionAware() // 使缓存操作与事务管理结合
                .build();
    }

    @Bean
    public RedisTemplate<String, ?> redisTemplate(RedisConnectionFactory factory, StringRedisSerializer strSerializer) {
        ObjectMapper objectMapper = getObjectMapper();
        // 对象存到 Redis 中需要带上类信息，否则从 Redis 取回的是 LinkedMap 而不是我们期望的原对象
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        Jackson2JsonRedisSerializer<?> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);

        RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(strSerializer);
        redisTemplate.setHashKeySerializer(strSerializer);
        redisTemplate.setValueSerializer(jsonSerializer);
        redisTemplate.setHashValueSerializer(jsonSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    private ObjectMapper getObjectMapper() {
        JavaTimeModule javaTimeModule = CustomJacksonModule.getJavaTimeModule(DEFAULT_DATETIME_PATTERN);
        return CustomJacksonModule.getObjectMapper(javaTimeModule);
    }

}

